Flask-OAuthlib 完全解説:FlaskアプリでのOAuth認証の導入と注意点

歴史的背景から現在のベストプラクティスまで

重要なお知らせ

Flask-OAuthlibは現在、積極的にメンテナンスされていません。 作者自身が、後継ライブラリである Authlib の利用を推奨しています。このブログ記事では Flask-OAuthlib について解説しますが、新規プロジェクトや既存プロジェクトのアップデートでは Authlib の利用を強く推奨します。

はじめに: Flask-OAuthlibとは?

Flask-OAuthlibは、PythonのWebフレームワークであるFlaskでOAuth認証・認可機能を実装するための拡張ライブラリです。

主な目的は以下の2点です:

  1. OAuthクライアント機能: Google、Twitter、GitHubなどの外部サービス (OAuthプロバイダ) のアカウントを利用して、自身のFlaskアプリケーションにログインする機能 (いわゆる「ソーシャルログイン」) を実装できます。
  2. 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 の主要な役割

  • リソースオーナー (Resource Owner): 保護されたリソース(例: ユーザーのプロフィール情報、写真など)の所有者。通常はエンドユーザー。
  • クライアント (Client): リソースオーナーの代わりに保護されたリソースへのアクセスを要求するアプリケーション。
  • 認可サーバー (Authorization Server): リソースオーナーの同意を得て、クライアントに対してアクセストークンを発行するサーバー。
  • リソースサーバー (Resource Server): 保護されたリソースをホストするサーバー。アクセストークンを検証し、リクエストに応答する。

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=Trueurl_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) に userclient 属性が追加され、アクセスできるようになります。

注意点:

  • プロバイダの実装はクライアントよりも複雑です。データモデルの設計、ユーザー認証の実装、各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 アプリケーションのセキュリティとユーザー体験において重要な役割を果たします。適切なライブラリを選択し、安全な実装を心がけましょう。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です