歴史的背景から現在のベストプラクティスまで
重要なお知らせ
Flask-OAuthlibは現在、積極的にメンテナンスされていません。 作者自身が、後継ライブラリである Authlib の利用を推奨しています。このブログ記事では Flask-OAuthlib について解説しますが、新規プロジェクトや既存プロジェクトのアップデートでは Authlib の利用を強く推奨します。
はじめに: Flask-OAuthlibとは?
Flask-OAuthlibは、PythonのWebフレームワークであるFlaskでOAuth認証・認可機能を実装するための拡張ライブラリです。
主な目的は以下の2点です:
- OAuthクライアント機能: Google、Twitter、GitHubなどの外部サービス (OAuthプロバイダ) のアカウントを利用して、自身のFlaskアプリケーションにログインする機能 (いわゆる「ソーシャルログイン」) を実装できます。
- OAuthプロバイダ機能: 自身のFlaskアプリケーションがOAuthプロバイダとなり、他のアプリケーションに対してAPIアクセスを認可する機能を提供します。
Flask-OAuthlibは、かつて広く使われていた Flask-OAuth
の後継として開発されました。内部的には oauthlib
というライブラリに依存しており、OAuth 1.0a と OAuth 2.0 の両方のプロトコルに対応しています。
Flaskとの親和性が高く、Flask-OAuth と同様のフレンドリーなAPIを提供することを目指して設計されました。
OAuthの基本概念
Flask-OAuthlibを理解する上で、OAuthの基本的な概念を知っておくことが重要です。OAuthは「認可」のためのプロトコルであり、「認証」そのものではありません。
OAuth 2.0 の主な認可フロー
OAuth 2.0 には、アプリケーションの種類や状況に応じていくつかの認可フロー(Grant Type)が定義されています。
フロー名 | 概要 | 主な用途 |
---|---|---|
認可コードフロー (Authorization Code Grant) | 最も一般的で安全なフロー。クライアントは認可サーバーから認可コードを取得し、それを使ってアクセストークンを取得する。クライアントシークレットを安全に保管できるサーバーサイドアプリケーション向け。 | Webサーバーアプリケーション |
インプリシットフロー (Implicit Grant) | 認可サーバーから直接アクセストークンを受け取るフロー。クライアントシークレットを安全に保管できないクライアント向けだったが、セキュリティ上の懸念から現在では非推奨であり、認可コードフロー + PKCE の利用が推奨される。 | JavaScriptなどを用いたブラウザベースのアプリケーション(過去) |
リソースオーナーパスワードクレデンシャルフロー (Resource Owner Password Credentials Grant) | ユーザーがクライアントに直接ユーザー名とパスワードを提供するフロー。信頼できるクライアント(例: 公式アプリ)でのみ使用されるべきで、一般的には推奨されない。 | 信頼できる自社製アプリケーション |
クライアントクレデンシャルフロー (Client Credentials Grant) | クライアント自身の認証情報(クライアントIDとシークレット)を使ってアクセストークンを取得するフロー。ユーザーの介入なしに、クライアントが自身の権限でAPIにアクセスする場合に使用する。 | マシン間のAPIアクセス |
デバイスコードフロー (Device Code Grant) | ブラウザや入力機能が制限されたデバイス(スマートTV、CLIツールなど)で利用されるフロー。 | スマートTV, IoTデバイス, CLIツール |
リフレッシュトークンフロー (Refresh Token Grant) | 有効期限の短いアクセストークンを再取得するためのフロー。リフレッシュトークンを用いて新しいアクセストークンを取得する。 | アクセストークンの有効期限が切れた後の再取得 |
Flask-OAuthlib はこれらのフロー、特に認可コードフローやクライアントクレデンシャルフローなどの実装をサポートします。
Flask-OAuthlibの主な機能
Flask-OAuthlibは、主にクライアント機能とプロバイダ機能を提供します。
- OAuth 1.0a, 1.0, 1.1, OAuth 2.0 クライアントサポート: 様々なバージョンのOAuthプロトコルに対応したクライアントを実装できます。
- フレンドリーなAPI: Flask-OAuthと同様の直感的なAPIを提供し、移行を容易にします。
- Flaskとの直接統合: Flaskアプリケーションにシームレスに組み込めます。Flaskのコンフィグレーションやセッションを利用できます。
- RESTful APIの簡単な呼び出し: 取得したトークンを使って保護されたAPIリソースに簡単にアクセスするためのメソッド (
get()
,post()
など) を提供します。 - OAuth1 プロバイダサポート: HMAC-SHA1 や RSA-SHA1 署名方式に対応したOAuth1プロバイダを構築できます。
- OAuth2 プロバイダサポート: Bearer トークンを用いたOAuth2プロバイダを構築できます。認可コードフロー、インプリシットフロー、リソースオーナーパスワードクレデンシャルフロー、クライアントクレデンシャルフローなどをサポートします。
インストール
Flask-OAuthlibはpipを使って簡単にインストールできます。
pip install Flask-OAuthlib
依存ライブラリである oauthlib
も自動的にインストールされます。
最新の安定版は PyPI で公開されていますが、開発版は GitHub リポジトリで管理されています。ただし、前述の通り、現在は Authlib の利用が推奨されています。
使い方 (クライアント編)
外部のOAuthプロバイダ (例: Google, Twitter, GitHub) を利用してユーザー認証を行うクライアント側の実装例です。
1. OAuthオブジェクトの初期化とリモートアプリケーションの登録
まず、OAuth
オブジェクトを作成し、接続したい外部サービス (リモートアプリケーション) を remote_app()
メソッドで登録します。
from flask import Flask, redirect, url_for, session, request, jsonify
from flask_oauthlib.client import OAuth
app = Flask(__name__)
# 必ず SECRET_KEY を設定してください
app.secret_key = 'your_secret_key_here' # 実際のアプリケーションでは環境変数などから読み込むべきです
oauth = OAuth(app)
# 例: GitHub OAuth2クライアントの登録
github = oauth.remote_app(
'github',
consumer_key='YOUR_GITHUB_CLIENT_ID', # GitHubから取得したClient ID
consumer_secret='YOUR_GITHUB_CLIENT_SECRET', # GitHubから取得したClient Secret
request_token_params={'scope': 'user:email'}, # 要求する権限 (スコープ)
base_url='https://api.github.com/',
request_token_url=None, # OAuth2では使用しない
access_token_method='POST',
access_token_url='https://github.com/login/oauth/access_token',
authorize_url='https://github.com/login/oauth/authorize'
)
# トークンを取得するための関数 (セッションに保存する例)
@github.tokengetter
def get_github_oauth_token():
return session.get('github_token')
重要な設定項目:
consumer_key
/consumer_secret
: OAuthプロバイダから発行されたクライアントIDとクライアントシークレット。request_token_params
: 認可リクエスト時に追加で送信するパラメータ。特にscope
で要求する権限を指定します。base_url
: APIリクエストのベースとなるURL。access_token_url
: アクセストークンを取得するためのエンドポイントURL。authorize_url
: ユーザーをリダイレクトさせる認可エンドポイントURL。@github.tokengetter
デコレータ: Flask-OAuthlibがAPIリクエストを行う際に、保存されたアクセストークンを取得するための関数を定義します。通常はセッションやデータベースから取得します。
2. ログイン処理のルート
ユーザーにログインを促し、OAuthプロバイダの認可ページへリダイレクトさせるルートを作成します。
@app.route('/login')
def login():
# ユーザーをGitHubの認可ページにリダイレクトさせる
# callbackには、認可後にリダイレクトされる自アプリのURLを指定
return github.authorize(callback=url_for('authorized', _external=True))
github.authorize()
を呼び出すと、ユーザーはGitHubのログイン・認可画面に遷移します。
3. コールバック処理のルート
ユーザーがOAuthプロバイダでの認可を完了すると、指定したコールバックURLにリダイレクトされます。このルートでアクセストークンを取得し、セッションなどに保存します。
@app.route('/login/authorized')
def authorized():
resp = github.authorized_response()
if resp is None or resp.get('access_token') is None:
return 'Access denied: reason={} error={}'.format(
request.args['error'],
request.args['error_description']
)
# アクセストークンをセッションに保存
session['github_token'] = (resp['access_token'], '') # (token, secret) の形式で保存 (OAuth2ではsecretは空)
session['github_user'] = github.get('user').data # 例: ユーザー情報を取得
return redirect(url_for('index'))
github.authorized_response()
で認可レスポンスを処理し、アクセストークンを取得します。取得したトークンは tokengetter
で定義した関数がアクセスできる場所に保存します(この例ではセッション)。
トークン取得後、github.get()
や github.post()
などを使って、保護されたAPIリソースにアクセスできます。
4. ログアウト処理のルート
セッションからトークン情報を削除します。
@app.route('/logout')
def logout():
session.pop('github_token', None)
session.pop('github_user', None)
return redirect(url_for('index'))
5. 保護されたルートと情報表示
ログイン状態を確認し、取得したユーザー情報を表示する例です。
@app.route('/')
def index():
if 'github_token' in session:
user_info = session.get('github_user')
if user_info:
return 'Logged in as id: {} name: {}
Logout'.format(user_info.get('id'), user_info.get('login'))
else:
# APIアクセスに失敗した場合など
return 'Logged in, but failed to fetch user info. Logout'
return 'You are not logged in. Log in with GitHub'
if __name__ == '__main__':
# HTTPSが推奨されますが、開発環境ではhttpでも動作します
# 重要: OAuthプロバイダに登録するコールバックURLと一致させる必要があります
app.run(debug=True, port=5000) # 必要に応じて host='localhost' を指定
注意点:
- Flaskアプリケーションには必ず
app.secret_key
を設定してください。セッション管理に必要です。本番環境では推測困難な秘密鍵を使用し、安全に管理してください。 - クライアントIDとクライアントシークレットは機密情報です。コードに直接埋め込まず、環境変数や設定ファイルなどから読み込むようにしてください。
- コールバックURL (
url_for('authorized', _external=True)
で生成されるURL) は、OAuthプロバイダに登録したものと完全に一致する必要があります (http/https, ドメイン名, ポート番号, パス)。 _external=True
をurl_for
に指定することで、絶対URLが生成されます。これはコールバックURLとして必要です。- ローカルでクライアントとプロバイダの両方をテストする場合、異なるポートで実行するなどして、セッションが衝突しないように注意が必要です。
使い方 (プロバイダ編)
Flask-OAuthlibを使って、自身のFlaskアプリケーションをOAuth 2.0プロバイダにする方法の概要です。クライアントアプリケーションからのアクセスを認可し、保護されたリソースへのアクセスを提供します。
1. OAuth2Providerオブジェクトの初期化
from flask import Flask, render_template, request, jsonify
from flask_sqlalchemy import SQLAlchemy # データモデルの例としてSQLAlchemyを使用
from flask_oauthlib.provider import OAuth2Provider
import datetime
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///oauth_provider.db' # 例: SQLite
app.config['SECRET_KEY'] = 'your_provider_secret_key' # クライアントと同様に必須
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
oauth = OAuth2Provider(app)
# --- データモデルの定義 (例) ---
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
# ... (パスワードハッシュなど、通常のユーザーモデルに必要な属性)
class Client(db.Model):
client_id = db.Column(db.String(40), primary_key=True)
client_secret = db.Column(db.String(55), unique=True, index=True, nullable=False)
user_id = db.Column(db.ForeignKey('user.id'))
user = db.relationship('User')
_redirect_uris = db.Column(db.Text)
_default_scopes = db.Column(db.Text)
# ... (クライアント名、クライアントタイプなど)
@property
def redirect_uris(self):
if self._redirect_uris:
return self._redirect_uris.split()
return []
@property
def default_redirect_uri(self):
return self.redirect_uris[0] if self.redirect_uris else None
@property
def default_scopes(self):
if self._default_scopes:
return self._default_scopes.split()
return []
@property
def client_type(self):
# 'public' または 'confidential' を返す
return 'public' # 例
@property
def allowed_grant_types(self):
# サポートするグラントタイプをリストで返す
return ['authorization_code', 'password', 'client_credentials', 'refresh_token']
@property
def allowed_response_types(self):
# サポートするレスポンスタイプをリストで返す
return ['code', 'token']
class Token(db.Model):
id = db.Column(db.Integer, primary_key=True)
client_id = db.Column(db.String(40), db.ForeignKey('client.client_id'), nullable=False)
client = db.relationship('Client')
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
user = db.relationship('User')
token_type = db.Column(db.String(40)) # 'Bearer' など
access_token = db.Column(db.String(255), unique=True)
refresh_token = db.Column(db.String(255), unique=True)
expires = db.Column(db.DateTime)
_scopes = db.Column(db.Text)
@property
def scopes(self):
if self._scopes:
return self._scopes.split()
return []
class Grant(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
user = db.relationship('User')
client_id = db.Column(db.String(40), db.ForeignKey('client.client_id'), nullable=False)
client = db.relationship('Client')
code = db.Column(db.String(255), index=True, nullable=False)
redirect_uri = db.Column(db.String(255))
expires = db.Column(db.DateTime)
_scopes = db.Column(db.Text)
@property
def scopes(self):
if self._scopes:
return self._scopes.split()
return []
# --- OAuth Provider の設定 ---
# 現在ログインしているユーザーを取得する関数 (Flask-Loginなどを使うのが一般的)
def get_current_user():
# ここで実際のログインユーザーを取得するロジックを実装
# 例: Flask-Login の current_user を使う
# from flask_login import current_user
# if current_user.is_authenticated:
# return User.query.get(current_user.id)
# return None
# ダミー実装
user = User.query.filter_by(username='testuser').first()
if not user:
user = User(username='testuser')
db.session.add(user)
db.session.commit()
return user
@oauth.clientgetter
def load_client(client_id):
return Client.query.filter_by(client_id=client_id).first()
@oauth.grantgetter
def load_grant(client_id, code):
return Grant.query.filter_by(client_id=client_id, code=code).first()
@oauth.grantsetter
def save_grant(client_id, code, request, *args, **kwargs):
# 認可コードを保存
expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=100)
grant = Grant(
client_id=client_id,
code=code['code'],
redirect_uri=request.redirect_uri,
_scopes=' '.join(request.scopes),
user=get_current_user(), # 現在のユーザーを取得
expires=expires
)
db.session.add(grant)
db.session.commit()
return grant
@oauth.tokengetter
def load_token(access_token=None, refresh_token=None):
if access_token:
return Token.query.filter_by(access_token=access_token).first()
elif refresh_token:
return Token.query.filter_by(refresh_token=refresh_token).first()
return None
@oauth.tokensetter
def save_token(token, request, *args, **kwargs):
# 既存のトークンを削除 (プロバイダのポリシーによる)
toks = Token.query.filter_by(client_id=request.client.client_id,
user_id=request.user.id).all()
for t in toks:
db.session.delete(t)
expires_in = token.get('expires_in')
expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=expires_in)
new_token = Token(
access_token=token['access_token'],
refresh_token=token.get('refresh_token'),
token_type=token['token_type'],
_scopes=token['scope'],
expires=expires,
client_id=request.client.client_id,
user_id=request.user.id
)
db.session.add(new_token)
db.session.commit()
return new_token
@oauth.usergetter
def get_user(username, password, *args, **kwargs):
# Password Credentials Grant のためのユーザー検証ロジック
# 本番環境ではパスワードハッシュを比較する
user = User.query.filter_by(username=username).first()
if user and user.check_password(password): # check_password は自分で実装する必要がある
return user
return None
プロバイダの実装には、クライアント、認可コード (Grant)、トークンなどを永続化するためのデータモデルと、それらを操作するための関数 (clientgetter
, grantgetter
, grantsetter
, tokengetter
, tokensetter
など) を OAuth2Provider
オブジェクトに登録する必要があります。
2. 認可エンドポイントとトークンエンドポイント
クライアントアプリケーションがアクセスするエンドポイントを定義します。
@app.route('/oauth/authorize', methods=['GET', 'POST'])
@oauth.authorize_handler
def authorize(*args, **kwargs):
# ユーザーがログインしていなければログインページにリダイレクト
user = get_current_user()
if not user:
# Flask-Loginなどのログイン処理へ
return redirect(url_for('login_page', next=request.url))
if request.method == 'GET':
# ユーザーに認可を求めるフォームを表示
client_id = kwargs.get('client_id')
client = Client.query.filter_by(client_id=client_id).first()
kwargs['client'] = client
kwargs['user'] = user
# scopes は kwargs['scopes'] にリストで入っている
return render_template('authorize_form.html', **kwargs) # 認可フォーム用のテンプレート
# POSTリクエストはユーザーがフォームで許可/拒否した場合
confirm = request.form.get('confirm', 'no')
return confirm == 'yes'
@app.route('/oauth/token', methods=['POST'])
@oauth.token_handler
def access_token():
# この関数自体は通常空で良い
# @oauth.token_handler がトークン発行処理を行う
return None
/oauth/authorize
: ユーザーにクライアントアプリケーションへのアクセス許可を求めるページを表示し、同意を得るエンドポイント。/oauth/token
: クライアントアプリケーションが認可コードやリフレッシュトークン、クレデンシャルなどを提示してアクセストークンを要求するエンドポイント。
@oauth.authorize_handler
や @oauth.token_handler
デコレータが、OAuthプロトコルの複雑な処理の多くを担当します。
3. 保護されたリソースエンドポイント
アクセストークンを使ってアクセスできるAPIエンドポイントを定義します。
@app.route('/api/me')
@oauth.require_oauth('email') # 要求するスコープを指定
def me(req):
# req.user にはアクセストークンに対応するユーザーオブジェクトが入る
user = req.user
return jsonify(id=user.id, username=user.username)
@app.route('/api/client')
@oauth.require_oauth() # クライアントクレデンシャル用のエンドポイント例
def client_api(req):
# req.client にはクライアントオブジェクトが入る
client = req.client
return jsonify(client_id=client.client_id, client_secret=client.client_secret) # 注意: シークレットを返すのは通常非推奨
# データベースの初期化 (初回実行時など)
@app.cli.command('init-db')
def init_db_command():
"""Clear the existing data and create new tables."""
db.create_all()
print('Initialized the database.')
# 必要に応じてテスト用のクライアントやユーザーを作成
if not User.query.filter_by(username='testuser').first():
test_user = User(username='testuser')
# test_user.set_password('password') # パスワード設定処理を実装する
db.session.add(test_user)
if not Client.query.filter_by(client_id='test_client').first():
client = Client(
client_id='test_client',
client_secret='test_secret', # 本番ではランダム生成する
_redirect_uris='http://localhost:5000/login/authorized http://127.0.0.1:5000/login/authorized', # 開発用コールバックURL例
_default_scopes='email profile',
user=User.query.filter_by(username='testuser').first() # クライアント所有者
)
db.session.add(client)
db.session.commit()
print('Added test user and client.')
if __name__ == '__main__':
app.run(debug=True, port=8000) # クライアントと異なるポートで実行
@oauth.require_oauth()
デコレータを使用することで、リクエストに有効なアクセストークンが含まれているか、また必要なスコープを持っているかを検証できます。検証が成功すると、リクエストオブジェクト (req
) に user
や client
属性が追加され、アクセスできるようになります。
注意点:
- プロバイダの実装はクライアントよりも複雑です。データモデルの設計、ユーザー認証の実装、各getter/setter関数の正確な実装が求められます。
- セキュリティは非常に重要です。トークンの安全な保存、HTTPSの強制、クライアント認証、スコープの適切な管理などを徹底する必要があります。
- 上記のコードは基本的な例であり、実際のアプリケーションではエラーハンドリング、ロギング、より堅牢なセキュリティ対策が必要です。
- ユーザー認証部分はFlask-Loginなどのライブラリと組み合わせることが一般的です。
Flask-OAuthlib の現在と代替ライブラリ
Flask-OAuthlib のメンテナンス状況
Flask-OAuthlib の開発は停滞しており、最後のリリース (バージョン 0.9.6) は 2020年9月 です。GitHubリポジトリやPyPIページでも、作者によって後継ライブラリである Authlib の利用が強く推奨されています。
Authlib は Flask-OAuthlib の作者によって開発されており、よりモダンで、機能が豊富で、活発にメンテナンスされています。
なぜ Authlib なのか?
- 活発な開発: Authlib は継続的に開発・改善されており、最新のOAuth/OIDC仕様への追従やセキュリティ修正が行われています。
- より良い設計: Flask-OAuthlib で得られた知見をもとに、より堅牢で拡張性の高い設計になっています。内部的に
requests
ライブラリを使用しており、HTTPリクエストの扱いが改善されています。 - 豊富な機能: OAuth 1.0, OAuth 2.0 クライアント/プロバイダに加え、OpenID Connect (OIDC), JSON Web Token (JWT), JWS, JWE, JWK, JWA など、認証・認可に関する幅広い仕様をサポートしています。
- フレームワーク非依存コア: コア部分は特定のWebフレームワークに依存せず、Flask, Django, Starlette, FastAPI など、様々なフレームワークとの連携が可能です。
- ドキュメントと例: Flask-OAuthlibよりも充実したドキュメントと豊富なサンプルコードが提供されています。
Flask-OAuthlib から Authlib への移行
Authlib の Flask クライアント API は Flask-OAuthlib と似ている部分が多く、移行は比較的容易に行えるように設計されています。主な変更点はインポートパスや一部の設定方法、デコレータの使用法などです。
例えば、クライアントの登録は以下のようになります。
# --- Flask-OAuthlib ---
# from flask_oauthlib.client import OAuth
# oauth = OAuth(app)
# github = oauth.remote_app(...)
# --- Authlib ---
from authlib.integrations.flask_client import OAuth
oauth = OAuth(app)
# または oauth = OAuth(); oauth.init_app(app) でも可
github = oauth.register(
name='github',
client_id='YOUR_GITHUB_CLIENT_ID',
client_secret='YOUR_GITHUB_CLIENT_SECRET',
access_token_url='https://github.com/login/oauth/access_token',
access_token_params=None,
authorize_url='https://github.com/login/oauth/authorize',
authorize_params=None,
api_base_url='https://api.github.com/',
client_kwargs={'scope': 'user:email'} # スコープ指定方法などが異なる場合がある
)
設定の読み込み方法や authorized_handler
デコレータの廃止など、細かい違いについては Authlib のドキュメントや移行ガイドを参照してください。
Authlib のドキュメント: https://docs.authlib.org/en/latest/client/flask.html
Authlib の GitHub リポジトリには、Flask, Django, FastAPI などを使ったサンプルコードも多数含まれています。
GitHub サンプル (クライアント): https://github.com/authlib/demo-oauth-client
GitHub サンプル (サーバー): https://github.com/authlib/example-oauth2-server
結論として、これからFlaskアプリケーションにOAuth機能を実装する場合、または既存のFlask-OAuthlib実装を更新する場合には、Authlib を選択することを強くお勧めします。
まとめ
Flask-OAuthlib は、かつて Flask で OAuth 認証・認可を実装するための便利なライブラリでした。クライアント機能とプロバイダ機能の両方を提供し、Flask との連携も容易でした。
しかし、現在では開発が停滞しており、作者自身が後継ライブラリである Authlib の利用を推奨しています。Authlib はよりモダンで高機能、活発にメンテナンスされており、Flask を含む多くの Python Web フレームワークで利用可能です。
この記事では Flask-OAuthlib の基本的な概念と使い方を解説しましたが、今後の開発においては Authlib を検討することが、より良い選択となるでしょう。OAuth は Web アプリケーションのセキュリティとユーザー体験において重要な役割を果たします。適切なライブラリを選択し、安全な実装を心がけましょう。